package dk.javacode.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

/**
 * An InvocationHandler that uses Interceptor annotations to call interceptors
 * before and after method invocation on the proxied object.
 * 
 * <pre>
 * In order to use this you should: 
 * - Make an interface (Test) and a class (TestImpl) implementing this interface. 
 * - Make an implementation of InterceptionHandler (MyInterceptor<Test>). 
 * - Annotate a method (from Test) in your class (TestImpl) with the Interceptor annotation (@SimpleInterceptor(MyInterceptor.class)). 
 * - Build a proxy of TestImpl i.e. by calling InterceptorProxy.build(Test.class, new TestImpl()). 
 * - Call the method you annotated
 * The before and after methods on your MyInterceptor class should 
 * now be invoked (before and after the method invocation on TestImpl).
 * </pre>
 * 
 */
public class InterceptorProxy implements InvocationHandler {

	private Object wrapped;

	private Map<String, List<InterceptionHandler<?>>> methodInterceptorMap = new HashMap<String, List<InterceptionHandler<?>>>();

	@SuppressWarnings("unchecked")
	/**
	 * Builds a new Dynamic Interceptor Proxy of the type "iface". The proxy will wrap the objectToWrap. That is method invocations will be passed 
	 * to objectToWrap.
	 * 
	 * @param iface
	 * @param objectToWrap
	 * @return
	 */
	public static <E> E buildProxy(Class<E> iface, Object objectToWrap) {
		return (E) Proxy.newProxyInstance(iface.getClassLoader(), new Class<?>[] { iface }, new InterceptorProxy(
				objectToWrap));
	}

	public InterceptorProxy(Object wrapped) {
		super();
		this.wrapped = wrapped;
		for (Method m : wrapped.getClass().getDeclaredMethods()) {
			SimpleInterceptor ia = m.getAnnotation(SimpleInterceptor.class);
			if (ia != null) {
				try {
					List<InterceptionHandler<?>> interceptors = new LinkedList<InterceptionHandler<?>>();
					for (Class<? extends InterceptionHandler<?>> interceptorClass : ia.value()) {
						InterceptionHandler<?> interceptor = interceptorClass.newInstance();
						if (!interceptor.accepts().isInstance(wrapped)) {
							throw new RuntimeException("Interceptor Class: " + interceptorClass
									+ " does not accept class: " + wrapped.getClass());
						}
						interceptors.add(interceptor);
					}
					methodInterceptorMap.put(m.getName(), interceptors);
				} catch (InstantiationException e) {
					throw new RuntimeException("Unable to instantiate interceptor for proxy.", e);
				} catch (IllegalAccessException e) {
					throw new RuntimeException("Unable to instantiate interceptor for proxy.", e);
				}
			}
		}
	}

	@SuppressWarnings({ "rawtypes", "unchecked" })
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		List<InterceptionHandler<?>> interceptors = methodInterceptorMap.get(method.getName());
		
		// Map to hold results of 'before' invocations.
		Map<InterceptionHandler<?>, Object> results = new HashMap<InterceptionHandler<?>, Object>();
		
		// Invoke all interceptors 'before'.
		for (InterceptionHandler interceptor : interceptors) {
			results.put(interceptor, interceptor.before(wrapped, args));
		}
		
		try {
			// Invoke method
			return method.invoke(wrapped, args);
		} finally {
			// Invoke all interceptors 'after' (continue even if one fails - but throw the exception when done)
			// After invocations are done in reverse order.
			Exception cleanupException = null;
			ListIterator<InterceptionHandler<?>> li = interceptors.listIterator(interceptors.size());
			while (li.hasPrevious()) {
				InterceptionHandler interceptor = li.previous();
				try {
					interceptor.after(wrapped, args, results.get(interceptor));
				} catch (Exception e) {
					cleanupException = new Exception("Exception in invocation of 'after' on interceptor: " + interceptor + ". Method: " + method.getName(), e);
				}
			}
			if (cleanupException != null) {
				throw cleanupException;
			}
		}
	}

}
